import json
import os
import numpy
from machine import *

VARIABLE = 0
LINEAR = 1
DEGREE = 7
RESOLUTION = 1000
STORE = '/netflix/blender/'

class Combiner(Machine):
  def __init__(self, machines, weights):
    self.mw = zip(machines, weights)
    self.total = sum(weights)
  def est_rating(self, mid, uid):
    return sum([m.est_rating(mid, uid) * w / self.total for m, w in self.mw])

def combine(ests, weights):
  total = sum(weights)
  return sum([est * w / total for est, w in zip(ests, weights)])

def poly_eval(perf, value):
  return sum([coeff * value ** (DEGREE - i) for i, coeff in enumerate(perf)])

class Blender(Machine):
  def __init__(self, store, chars, machines):
    self.store = store
    self.chars = dict([(c.name, c) for c in chars])
    self.machines = dict([(m.name, m) for m in machines])
    self.perf = {}
    self.overall_perf = {}
    self.method = VARIABLE
  
  def eval_chars(self, set, quiet=False):
    self.perf = {}
    self.overall_perf = {}
    for m_name, m in self.machines.items():
      if not quiet:
        print "evaluating %s" % m_name
      self.overall_perf[m_name] = m.calc_rmse(quiet=True, outset=set)
      self.perf[m_name] = {}
      for c_name, c in self.chars.items():
        if not quiet:
          print "  on characteristic %s" % c_name
        bins = [[0., 0] for x in range(RESOLUTION+1)]
        for mid, uid, r in set.iterMovieIDsUserIDsRatings():
          bin_n = int(RESOLUTION * (c.get_char(uid, mid) - c.min) / (c.max - c.min))
          bins[bin_n][0] += (m.est_rating(mid, uid) - r) ** 2
          bins[bin_n][1] += 1
        rmse_s = [ (se / n) ** .5 for se, n in bins if n]
        x_s = [ c.min + i*(c.max-c.min)/RESOLUTION for i, (se, n) in enumerate(bins) if n]
        self.perf[m_name][c_name] = numpy.polyfit(x_s, rmse_s, DEGREE).tolist()
  
  def find_linear_weights(self, set):
    print "finding linear weights"
    ests = []
    real = []
    m_names, machines = self.machines.keys(), self.machines.values()
    for mid, uid, r in set.iterMovieIDsUserIDsRatings():
      ests.append([m.est_rating(mid, uid) for m in machines])
      real.append([r])
    
    ests = numpy.matrix(ests)
    real = numpy.matrix(real)
    
    self.weights = dict(zip(m_names, (numpy.linalg.pinv(ests) * real).flatten().tolist()[0]))
    print "linear weights:", self.weights
  
  def get_perf(self, c_name, m_name, value):
    est_rmse = poly_eval(self.perf[m_name][c_name], value)
    return est_rmse
  
  def est_rating_variable(self, mid, uid):
    ests = []
    weights = []
    for m_name in self.machines:
      ests.append(self.machines[m_name].est_rating(mid, uid))
      weight = self.weights[m_name]
      for c_name, c in self.chars.items():
        weight += (self.overall_perf[m_name] - \
                   self.get_perf(c_name, m_name, c.get_char(uid, mid))) / 100.
      weights.append(weight)
    return combine(ests, weights)
  
  def est_rating_linear(self, mid, uid):    
    r = 0.
    for m_name, m in self.machines.items():
      r += m.est_rating(mid, uid) * self.weights[m_name]
    return r
  
  def est_rating(self, mid, uid):
    if self.method == VARIABLE:
      return self.est_rating_variable(mid, uid)
    elif self.method == LINEAR:
      return self.est_rating_linear(mid, uid)
  
  def write_data(self):
    for m_name, m in self.machines.items():
      f = file(self.store + m_name + '_overall', 'w')
      json.dump(self.overall_perf[m_name], f)
      f.close()
      for c_name, c in self.chars.items():
        f = file(self.store + c_name + "@" + m_name, 'w')
        json.dump(self.perf[m_name][c_name], f)
        f.close()
  
  def read_data(self):
    for filename in os.listdir(self.store):
      if "@" in filename:
        c_name, m_name = filename.split("@")
        if m_name not in self.perf:
          self.perf[m_name] = {}
        self.perf[m_name][c_name] = json.load(file(self.store + filename))
    for m_name in self.perf:
      self.overall_perf[m_name] = json.load(file(self.store + m_name + '_overall'))

class Characteristic(object):
  pass